﻿using Microsoft.Win32;
using SourceMaster.Model;
using SourceMaster.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LibGit2Sharp;
using System.Windows;
using SourceMaster.UserControls;
using System.Windows.Threading;
using System.Windows.Controls;
using System.Collections;
using System.Windows.Media.Effects;


namespace SourceMaster.ViewModel
{
    public class RepoViewModel : MyViewModel
    {
        #region Properties and fields
        public string Name { get; set; }
        // the full path of repository
        public string RepoFullPath { get; set; }
        public bool NotOpened { get; set; }

        public EnhancedObservableCollection<MyCommit> MyCommits { get; set; }

        public EnhancedObservableCollection<StatusItem> StatusItemsStaged { get; set; }

        public EnhancedObservableCollection<StatusItem> StatusItemsUnstaged { get; set; }

        public EnhancedObservableCollection<MyBranch> MyBranches { get; set; }

        public EnhancedObservableCollection<MyTag> MyTags { get; set; }

        public EnhancedObservableCollection<MyRemote> MyRemotes { get; set; }

        public EnhancedObservableCollection<SubModule> SubModules { get; set; }

        public EnhancedObservableCollection<MyStash> MyStashes { get; set; }

        public EnhancedObservableCollection<RecentCommitMessage> RecentCommitMesages { get; set; }

        private MyBranch head;

        public MyBranch Head
        {
            get { return head; }
            set
            {
                head = value;
                RaisePropertyChanged("Head");
            }
        }

        public int CommitsPerPage { get; set; }
        public int RecentCommitMessagesCount { get; set; }

        public string statusItemDiff;

        public string StatusItemDiff
        {
            get { return statusItemDiff; }
            set
            {
                statusItemDiff = value;
                RaisePropertyChanged("StatusItemDiff");
            }
        }

        delegate void ReloadStatusDelegate(object sender, FileSystemEventArgs e);

        #endregion

        public RepoViewModel()
        {
            MyCommits = new EnhancedObservableCollection<MyCommit> { };
            StatusItemsStaged = new EnhancedObservableCollection<StatusItem> { };
            StatusItemsUnstaged = new EnhancedObservableCollection<StatusItem> { };
            MyBranches = new EnhancedObservableCollection<MyBranch> { };
            MyTags = new EnhancedObservableCollection<MyTag> { };
            MyRemotes = new EnhancedObservableCollection<MyRemote> { };
            SubModules = new EnhancedObservableCollection<SubModule> { };
            MyStashes = new EnhancedObservableCollection<MyStash> { };
            RecentCommitMesages = new EnhancedObservableCollection<RecentCommitMessage> { };
            CommitsPerPage = 150;
            RecentCommitMessagesCount = 10;

            AddNoteCommand = new DelegateCommand(AddNote);
            CheckOutCommand = new DelegateCommand(CheckOut);
            CreateBranchCommand = new DelegateCommand(CreateBranch);
            CommitCommand = new DelegateCommand(CommitChanges, CommitChanges_CanExecute);
            CopyHashCommand = new DelegateCommand(CopyHash);
            DeleteFileCommand = new DelegateCommand(DeleteFile);
            DeleteTagCommand = new DelegateCommand(DeleteTag);
            ExportPatchCommand = new DelegateCommand(ExportPatch);
            OpenAboutCommand = new DelegateCommand(OpenAbout);
            ResetSoftCommand = new DelegateCommand(ResetSoft);
            ResetMixedCommand = new DelegateCommand(ResetMixed);
            StageUnstageCommand = new DelegateCommand(StageUnstage);

            StatusItemDiff = "";

        }

        #region Commands
        public DelegateCommand AddNoteCommand { get; private set; }
        public DelegateCommand CheckOutCommand { get; private set; }
        public DelegateCommand CreateBranchCommand { get; private set; }
        public DelegateCommand CreateTagCommand { get; private set; }
        public DelegateCommand CommitCommand { get; private set; }
        public DelegateCommand CopyHashCommand { get; private set; }
        public DelegateCommand DeleteFileCommand { get; private set; }
        public DelegateCommand DeleteTagCommand { get; private set; }
        public DelegateCommand ExportPatchCommand { get; private set; }
        public DelegateCommand OpenAboutCommand { get; private set; }
        public DelegateCommand ResetSoftCommand { get; private set; }
        public DelegateCommand ResetMixedCommand { get; private set; }
        public DelegateCommand StageUnstageCommand { get; private set; }

        // patch can be a file format.
        // this function will export the changeset as a .patch file
        public void ExportPatch(object action)
        {
            var myCommit = action as MyCommit;
            if (myCommit == null)
                return;

            var dialog = new SaveFileDialog
            {
                FileName = myCommit.Description.Right(72),
                DefaultExt = ".patch",
                Filter = "Patch files|.*patch"
            };

            if (dialog.ShowDialog() == false)
                return;

            var fName = dialog.FileName;

            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {

                    File.WriteAllText(fName, RepoUtil.GetPatchForCommit(repo, myCommit).Content);
                }
            });
        }

        public void CopyPatch(object action)
        {
            var myCommit = action as MyCommit;
            if (myCommit == null)
                return;
            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    Clipboard.SetText(RepoUtil.GetPatchForCommit(repo, myCommit).Content);
                }
            });
        }

        // checkout a commit/branch/tag
        public void CheckOut(object action)
        {
            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    if (action is MyTag)
                        repo.Checkout(((MyTag)action).CanoicalName);
                }
            });
        }

        public void AddNote(object action)
        {
            var dialog = new PromptDialog
            {
                Title = "Add a note",
                Message = "Enter the note to add for the commit:"
            };

            dialog.ShowDialog();

            if (dialog.DialogResult != true)
                return;

            var myCommit = action as MyCommit;
            var response = dialog.ResponseText;

            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    repo.Notes.Add(myCommit.ObjectId, response, MyGitConfig.NoteNamespace);
                    LoadEntireRepository();
                }
            });
        }

        // paramater type MyCommit
        public void CopyHash(object action)
        {
            var myCommit = action as MyCommit;
            if (myCommit == null)
                return;
            Clipboard.SetText(myCommit.Hash);
        }

        // parameter type MyCommit
        public void CreateTag(object action)
        {
            var myCommit = action as MyCommit;
            var dialog = new PromptDialog
            {
                Title = "Create a new tag",
                Message = "Enter the name for the tag:"
            };

            dialog.ShowDialog();

            if (dialog.DialogResult != true || myCommit == null)
                return;
            var response = dialog.ResponseText;

            //Task.Run(() ={
            //    using (var repo = new Repository(RepoFullPath))
            //    {
            //        repo.Tags.Add(response, myCommit.originCommit);
            //    }
            //});
            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    Tag newTag = repo.Tags.Add(response, myCommit.originCommit);

                    if (newTag != null)
                    {
                        MyTag myTag = MyTag.Create(repo, newTag);
                        MyTags.Add(myTag);
                    }
                    //MyTag mytag = MyTag.Create(repo, newTag);
                    //MyTags.Add(newTag);
                    //MyTags.Add()
                    LoadEntireRepository();
                }
            });
        }

        // parameter type MyTag
        public void DeleteTag(object action)
        {
            var myTag = action as MyTag;
            if (myTag == null)
                return;

            var dialog = new ConfirmDialog
            {
                Title = "Deleting a tag",
                Message = String.Format("Are you sure you want to delete this tag ({0})", myTag.Name)
            };

            dialog.ShowDialog();

            var pressedButton = dialog.PressedButton;
            if (pressedButton == null || ((string)pressedButton.Content) != "OK")
                return;

            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    repo.Tags.Remove(myTag.OriginTag);

                    LoadTags();

                    Application.Current.Dispatcher.BeginInvoke(
                        DispatcherPriority.Normal,
                        (Action)(() => myTag.Target.Tags.Remove(myTag))
                    );
                }
            });
        }

        public void ResetMixed(object action)
        {
            var myCommit = action as MyCommit;
            if(myCommit == null)
            {
                return;
            }

            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    // hash?
                    repo.Reset(ResetMode.Mixed, myCommit.originCommit);
                    LoadEntireRepository();
                }
            });
        }

        public void ResetSoft(object action)
        {
            var myCommit = action as MyCommit;
            if (myCommit == null)
                return;
            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    repo.Reset(ResetMode.Soft, myCommit.originCommit);
                    LoadEntireRepository();
                }
            });
        }

        public void CreateBranch(object action)
        {
            var dialog = new PromptDialog
            {
                Title = "Creating a new branch",
                Message = "Please give a name for your newbranch:"
            };
            dialog.ShowDialog();

            if (dialog.DialogResult != true)
                return;

            var response = dialog.ResponseText;

            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    repo.Branches.Add(response, repo.Head.Tip);
                }
                LoadEntireRepository();
            });
        }

        public void StageUnstage(object action)
        {
            var collection = (IList)action;
            var items = collection.Cast<StatusItem>();

            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    foreach (var item in items)
                    {
                        if (item.GenericStatus == "Staged")
                            //repo.Unstage(RepoFullPath + "/" + item.FileName);
                            repo.Unstage(RepoFullPath + Path.DirectorySeparatorChar + item.FileName);
                        else
                            repo.Stage(RepoFullPath + Path.DirectorySeparatorChar + item.FileName);
                    }
                }
            });
        }

        public void CommitChanges(object action)
        {
            var commitMessage = ((TextBox)action).Text;
            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    RepositoryExtensions.Commit(repo, commitMessage);

                    LoadEntireRepository();

                    Application.Current.Dispatcher.BeginInvoke(
                        (Action)(() => UIHelper.FindChild<TextBox>(Application.Current.MainWindow, "CommitMessageTextBox").Clear()));
                }
            });
        }

        public bool CommitChanges_CanExecute(object action)
        {
            var textBox = ((TextBox)action);
            if (textBox == null)
                return false;

            var commitMessage = textBox.Text;

            if (commitMessage != null && StatusItemsStaged.Count > 0)
                return commitMessage.Length > 0;

            return false;
        }

        public void DeleteFile(object action)
        {
            var dialog = new ConfirmDialog
            {
                Title = "Delete files",
                Message = "Are you sure you want to delete the selected files(s)?\r\n\r\nYou may lose your changes!",
                ButtonSet = ConfirmDialog.BtnType.OK_CANCEL
            };
            dialog.ShowDialog();
            var pressedButton = (Button)dialog.PressedButton;
            if(pressedButton != null &&((string)pressedButton.Content)=="OK")
            {
                var collection = (IList)action;
                var items = collection.Cast<StatusItem>();
                Repository repo = null;

                // Loop through all selected status items and remove the files physically (and in some cases also from the repository).
                foreach (StatusItem item in items)
                {
                    // TODO: --cached ?

                    File.Delete(RepoFullPath + Path.DirectorySeparatorChar + item.FileName);

                    if (!item.Status.HasFlag(LibGit2Sharp.FileStatus.Untracked))
                    {
                        if (repo == null)
                            repo = new LibGit2Sharp.Repository(RepoFullPath);

                        repo.Unstage(RepoFullPath + Path.DirectorySeparatorChar + item.FileName);
                    }
                }

                if (repo != null)
                    repo.Dispose();
            }
        }

        private void OpenAbout(object action)
        {
            Window about = new About();

            var blur = new BlurEffect
            {
                Radius = 4
            };

            Application.Current.MainWindow.Effect = blur;

            about.ShowDialog();

            Application.Current.MainWindow.Effect = null;
        }
        #endregion

        #region Loading and construction
        public bool Init()
        {
            if(NotOpened == false)
            {
                try
                {
                    LoadEntireRepository();
                }
                catch(Exception)
                {
                    return false;
                }
            }
            return true;
        }

        public void LoadEntireRepository()
        {
            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    LoadTags(repo);
                    LoadBranchesAndCommits(repo);
                    LoadRepositoryStatus(repo);
                    LoadRecentCommitMessages();

                    ListenToDirectoryChanges();
                }
            });
        }

        private void LoadBranchesAndCommits(Repository repo = null)
        {
            var dispose = false;
            if(repo == null)
            {
                repo = new Repository(RepoFullPath);
                dispose = true;
            }

            MyCommits.DisableNotifications();
            MyBranches.DisableNotifications();

            MyCommits.Clear();
            var myCommitList = new List<MyCommit>();
            foreach(var commit in repo.Commits.QueryBy(new CommitFilter { Since = repo.Branches}).Take(CommitsPerPage))
            {
                myCommitList.Add(MyCommit.Create(repo, commit, MyTags));
            }
            MyCommits.AddRange(myCommitList);

            MyBranches.Clear();
            foreach(var branch in repo.Branches)
            {
                var b = MyBranch.Create(this, repo, branch);
                MyBranches.Add(b);
            }

            foreach(var myBranch in MyBranches)
            {
                if(repo.Head.Name == myBranch.Name)
                {
                    Head = myBranch;
                    myBranch.Tip.IsHead = true;
                }
                myBranch.PostProcess(MyBranches, MyCommits);
            }

            foreach(var myCommit in MyCommits)
            {
                if(Head == null && repo.Head.Tip.Sha == myCommit.Hash)
                {
                    Head = new DetachedHead
                    {
                        Tip = myCommit
                    };
                    myCommit.IsHead = true;
                }

                myCommit.PostProcess(MyCommits, MyBranches);
            }

            foreach(var myBranch in MyBranches)
            {
                RepoUtil.IncrementCommitVisualPosition(myBranch.Tip);
            }

            // Fire notification for the collections on the UI thread.
            Application.Current.Dispatcher.Invoke(
                DispatcherPriority.Normal,
                (Action)(() =>
                {
                    MyCommits.EnableNotifications(true);
                    MyBranches.EnableNotifications(true);

                    var tabControl = UIHelper.FindChild<TabControl>(Application.Current.MainWindow, "RepositoryTabs");
                    var changesetHistory = UIHelper.FindChild<ChangeSetHistory>(tabControl);
                }));
            if (dispose)
                repo.Dispose();
        }

        private void LoadTags(Repository repo = null)
        {
            var dispose = false;
            if(repo == null)
            {
                repo = new Repository(RepoFullPath);
                dispose = true;
            }

            MyTags.DisableNotifications();

            MyTags.Clear();

            foreach(var tag in repo.Tags)
            {
                var t = MyTag.Create(repo, tag);
                if (t.HasCommitAsTarget)
                    MyTags.Add(t);
            }

            // Fire notifications for the Tags collection on the UI thread
            Application.Current.Dispatcher.Invoke(
                DispatcherPriority.Normal,
                (Action)(() => MyTags.EnableNotifications(true)));

            if (dispose)
                repo.Dispose();
        }

        private void LoadRepositoryStatus(Repository repo = null)
        {
            var dispose = false;
            if(repo == null)
            {
                repo = new Repository(RepoFullPath);
                dispose = true;
            }

            // A small performance boost.
            StatusItemsStaged.DisableNotifications();
            StatusItemsUnstaged.DisableNotifications();

            var itemList = new List<StatusItem>();

            var statuses = repo.RetrieveStatus();
            
            foreach(var fileStatus in statuses)
            {
                foreach(FileStatus value in Enum.GetValues(typeof(LibGit2Sharp.FileStatus)))
                {
                    var isSet = fileStatus.State.HasFlag(value);

                    if (isSet = false || value == FileStatus.Unaltered || value == FileStatus.Ignored)
                        continue;

                    var fileFullPath = RepoFullPath + Path.DirectorySeparatorChar + fileStatus.FilePath;

                    var item = new StatusItem
                    {
                        FileName = fileStatus.FilePath,
                        Status = value,
                        Size = FileUtil.GetFormattedFileSize(fileFullPath),
                        IsBinary = FileUtil.IsBinaryFile(fileFullPath) ? "Yes" : "-"
                    };

                    itemList.Add(item);
                }
            }

            StatusItemsStaged.AddRange(itemList.Where(s => s.IsStaged));
            StatusItemsUnstaged.AddRange(itemList.Where(s => !s.IsStaged));

            if (dispose)
                repo.Dispose();

            Application.Current.Dispatcher.Invoke(
                DispatcherPriority.Normal,
                (Action)(() =>
                {
                    StatusItemsStaged.EnableNotifications(true);
                    StatusItemsStaged.EnableNotifications(true);
                }));
        }

        private void LoadRecentCommitMessages()
        {
            RecentCommitMesages.DisableNotifications();
            RecentCommitMesages.Clear();

            foreach (var myCommit in MyCommits.Take(RecentCommitMessagesCount))
                RecentCommitMesages.Add(new RecentCommitMessage(myCommit.ShortDescription));

            Application.Current.Dispatcher.Invoke(
                DispatcherPriority.Normal,
                (Action)(() => RecentCommitMesages.EnableNotifications(true)));

        }


        #endregion

        public void UpdateStatusItemDiff(IList collection)
        {
            if (NotOpened)
                return;

            var diff = "";
            var items = collection.Cast<StatusItem>();

            Task.Run(() =>
            {
                using (var repo = new Repository(RepoFullPath))
                {
                    diff += repo.Diff.Compare<Patch>(repo.Head.Tip.Tree, DiffTargets.Index);
                }
                StatusItemDiff = diff;
            });
        }

        private void ListenToDirectoryChanges()
        {
            var watcher = new FileSystemWatcher();

            ReloadStatusDelegate reloadStatusDelegate = delegate (object sender, FileSystemEventArgs e)
            {
                Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (Action)(() => LoadRepositoryStatus()));
            };

            watcher.Changed += new FileSystemEventHandler(reloadStatusDelegate);
            watcher.Deleted += new FileSystemEventHandler(reloadStatusDelegate);
            watcher.Renamed += new RenamedEventHandler(reloadStatusDelegate);
            watcher.Created += new FileSystemEventHandler(reloadStatusDelegate);
            watcher.Path = RepoFullPath;
            watcher.EnableRaisingEvents = true;
        }

    }
}
